Redes Neuronales Artificiales#

import sys
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
import datetime
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from tensorflow import keras
from keras.layers import Dense, Input, Dropout
from keras.optimizers import SGD
from keras.models import Model
from keras.models import load_model
from keras.callbacks import ModelCheckpoint
import os
from sklearn.metrics import r2_score
from sklearn.metrics import mean_absolute_error
import warnings
warnings.filterwarnings("ignore")
url = 'https://raw.githubusercontent.com/evgomez98/wind_speed/main/wind_dataset.csv'
df = pd.read_csv(url)

Para iniciar, se realiza la divisón del data set

df['DATE'] = pd.to_datetime(df['DATE'], format='%Y-%m-%d')

split_date_train = datetime.datetime(year=1977, month=1, day=1)
split_date_test = datetime.datetime(year=1978, month=1, day=1)

df_train = df.loc[df['DATE'] < split_date_train]
df_val = df.loc[(df['DATE'] >= split_date_train) & (df['DATE'] < split_date_test)]
df_test = df.loc[df['DATE'] >= split_date_test]

print('Shape of train:', df_train.shape)
print('Shape of validation:', df_val.shape)
print('Shape of test:', df_test.shape)
Shape of train: (5844, 9)
Shape of validation: (365, 9)
Shape of test: (365, 9)
df_val.reset_index(drop=True, inplace=True)
df_test.reset_index(drop=True, inplace=True)

Ahora necesitamos generar los regresores (X) y la variable objetivo (y) para el entrenamiento, validación y testeo. La matriz bidimensional de regresores y la matriz unidimensional objetivo se crean a partir de la matriz unidimensional original de WIND.

def makeXy(ts, nb_timesteps):
    X = []
    y = []
    for i in range(nb_timesteps, ts.shape[0]):
        if i-nb_timesteps <= 4:
            print(i-nb_timesteps, i-1, i)
        X.append(list(ts.loc[i-nb_timesteps:i-1])) #Regressors
        y.append(ts.loc[i]) #Target
    X, y = np.array(X), np.array(y)
    return X, y
X_train, y_train = makeXy(df_train['WIND'],5)
print('Shape of train arrays:', X_train.shape, y_train.shape)
0 4 5
1 5 6
2 6 7
3 7 8
4 8 9
Shape of train arrays: (5839, 5) (5839,)
X_val, y_val = makeXy(df_val['WIND'], 5)
print('Shape of val arrays:', X_val.shape, y_val.shape)
0 4 5
1 5 6
2 6 7
3 7 8
4 8 9
Shape of val arrays: (360, 5) (360,)
X_test, y_test = makeXy(df_test['WIND'], 5)
print('Shape of test arrays:', X_test.shape, y_test.shape)
0 4 5
1 5 6
2 6 7
3 7 8
4 8 9
Shape of test arrays: (360, 5) (360,)

Redes multicapa - MLP#

Se crea una capa de entrada en un modelo de red neuronal. donde se específica la forma de los datos de entrada. En este caso, significa que los datos de entrada tendrán 5 dimensiones. dtype=’float64’ especifica el tipo de datos de los elementos de la capa de entrada, que este caso sería números de punto flotante de 64 bits.

input_layer = Input(shape=(5,), dtype='float64')

Se crean las capas ocultas con función de activación ReLu.

dense1 = Dense(32, activation='relu')(input_layer)
dense2 = Dense(16, activation='relu')(dense1)
dense3 = Dense(16, activation='relu')(dense2)

Se añade una capa Dropout antes de la capa de salida, con p = 20% de las características de entrada seleccionadas aleatoriamente.

dropout_layer = Dropout(0.2)(dense3)

Finalizamos con la capa de salida, con una sola neurona, con función de activación lineal.

output_layer = Dense(1, activation='linear')(dropout_layer)
ts_model = Model(inputs=input_layer, outputs=output_layer)
ts_model.compile(loss='mean_squared_error', optimizer='adam')
ts_model.summary()
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 5)]               0         
                                                                 
 dense (Dense)               (None, 32)                192       
                                                                 
 dense_1 (Dense)             (None, 16)                528       
                                                                 
 dense_2 (Dense)             (None, 16)                272       
                                                                 
 dropout (Dropout)           (None, 16)                0         
                                                                 
 dense_3 (Dense)             (None, 1)                 17        
                                                                 
=================================================================
Total params: 1,009
Trainable params: 1,009
Non-trainable params: 0
_________________________________________________________________

Se configura la selección del modelo para capturar el mejor modelo basado en la validación de pérdida:

save_weights_at = os.path.join('keras_models', 'WS_MLP_weights.{epoch:02d}-{val_loss:.4f}.keras')
save_best = ModelCheckpoint(save_weights_at, monitor='val_loss', verbose=0,
                            save_best_only=True, save_weights_only=False, mode='min', save_freq='epoch');
import os
from joblib import dump, load

Y entrenamos el MLP:

history_WS = None

if os.path.exists('history_WS.joblib'):
    history_WS = load('history_WS.joblib')
    print("El archivo 'history_WS.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_WS = ts_model.fit(x=X_train, y=y_train, batch_size=32, epochs=30,
             verbose=2, callbacks=[save_best], validation_data=(X_val, y_val),
             shuffle=True);
    dump(history_WS.history, 'history_WS.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_WS.joblib'.")
El archivo 'history_WS.joblib' ya existe. Se ha cargado el historial del entrenamiento.
import os
import re
from tensorflow.keras.models import load_model

Ahora extreamos el mejor modelo:

model_dir = 'keras_models'
files = os.listdir(model_dir)
pattern = r"WS_MLP_weights\.(\d+)-([\d\.]+)\.keras"

best_val_loss = float('inf')
best_model_file = None
best_model = None

for file in files:
    match = re.match(pattern, file)
    if match:
        epoch = int(match.group(1))
        val_loss = float(match.group(2))
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_model_file = file

if best_model_file:
    best_model_path = os.path.join(model_dir, best_model_file)
    print(f"Cargando el mejor modelo: {best_model_file} con val_loss: {best_val_loss}")
    best_model = load_model(best_model_path)
else:
    print("No se encontraron archivos de modelos que coincidan con el patrón.")
Cargando el mejor modelo: WS_MLP_weights.30-16.5359.keras con val_loss: 16.5359
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from statsmodels.stats.diagnostic import acorr_ljungbox
from scipy.stats import normaltest

index = 'MLP val model'

val_result = best_model.predict(X_val)
val_pred = val_result[:, 0]

x_val = df_val['WIND'].loc[5:]

mae = mean_absolute_error(x_val, val_pred)
sse = np.sum((x_val - val_pred) ** 2)
mape = np.mean(np.abs((x_val-val_pred) / x_val)) * 100
msd = mean_squared_error(x_val, val_pred)
r2 = r2_score(x_val, val_pred) 

ljung_box = acorr_ljungbox(x_val - val_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(x_val- val_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
 1/12 [=>............................] - ETA: 2s

12/12 [==============================] - 0s 3ms/step
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
MLP val model 3.236883 5952.907487 34.57921 16.535854 0.293768 0.351058 0.00066

Para la validación del modelo MLP, la métrica MAE es de 3.21, indicando que, en promedio, las predicciones están desviadas por 3.21 nudos respecto a los valores reales. La SSE de 5978.44 sugiere que, aunque hay errores acumulativos, estos son moderados en comparación con otros modelos evaluados. Un MAPE de 33.68% revela un nivel de precisión más razonable, aunque aún distante del ideal. La MSD de 16.61 indica que los errores están relativamente controlados. El coeficiente R² positivo de 0.29 sugiere una relación explicativa leve entre las predicciones y los datos reales, lo cual es una mejora respecto a los valores negativos observados en otros modelos. Los p-valores de las pruebas de Ljung-Box (0.514) y de normalidad (0.0003) indican que no hay una autocorrelación significativa en los residuos, aunque estos siguen sin ser normales. En conjunto, estas métricas sugieren que el modelo MLP presenta una mejora en precisión, aunque todavía con ciertas limitaciones para capturar los patrones en la serie temporal.

index = 'MLP test model'

test_result =  best_model.predict(X_test)
test_pred = test_result[:, 0]  

x_test = df_test['WIND'].loc[5:]

mae = mean_absolute_error(x_test, test_pred)
sse = np.sum((x_test - test_pred) ** 2)
mape = np.mean(np.abs((x_test - test_pred) / x_test)) * 100
msd = mean_squared_error(x_test, test_pred)
r2 = r2_score(x_test,test_pred) 

ljung_box = acorr_ljungbox(x_test - test_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(x_test - test_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
 1/12 [=>............................] - ETA: 0s

12/12 [==============================] - 0s 1ms/step
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
MLP test model 3.360381 6390.525462 62.574827 17.75146 0.342934 0.770134 0.000371

Para el modelo MLP, el MAE de 3.36 indica que, en promedio, las predicciones difieren de los valores reales en 3.36 nudos, lo cual sugiere un nivel de precisión moderado. La SSE de 6321.54 refleja un error acumulativo que, aunque presente, es relativamente bajo en comparación con otros modelos. Un MAPE de 62.80% implica que la precisión porcentual aún muestra margen de mejora, aunque sigue siendo razonable para ciertos contextos de series temporales. La MSD de 17.56 corrobora que los errores no son extremadamente altos, mientras que el R² de 0.35 señala una mejoría en la capacidad explicativa del modelo, indicando una relación algo más significativa entre las predicciones y los valores reales. Los p-valores de Ljung-Box (0.789) y de la prueba de normalidad (0.0002) sugieren una menor autocorrelación en los residuos, aunque persiste la no normalidad, lo que puede influir en la estabilidad del modelo. En general, este modelo MLP presenta una precisión y ajuste aceptables, con potencial para capturar algunos patrones de la serie temporal.

import plotly.graph_objects as go

def plot_model(train, val, test, test_pred, title):
  
    train = np.array(train)
    val = np.array(val)
    test = np.array(test)
    test_pred = np.array(test_pred)

   
    train_index = np.arange(0, len(train))
    val_index = np.arange(len(train), len(train) + len(val))
    test_index = np.arange(len(train) + len(val), len(train) + len(val) + len(test))
    test_pred_index = test_index  

    fig = go.Figure()
    fig.add_trace(go.Scatter(x=train_index, y=train, mode='lines', name='Train', line=dict(color='blue')))
    fig.add_trace(go.Scatter(x=val_index, y=val, mode='lines', name='Validation', line=dict(color='orange')))
    fig.add_trace(go.Scatter(x=test_index, y=test, mode='lines', name='Test', line=dict(color='green')))
    fig.add_trace(go.Scatter(x=test_pred_index, y=test_pred, mode='lines', name='Prediction', line=dict(color='red', dash='dash')))

    fig.update_layout(
        title=title,
        xaxis_title="Observaciones",
        yaxis_title="Valores",
        legend=dict(x=0, y=1, traceorder="normal"),
        plot_bgcolor='rgba(0,0,0,0)',
        width=900, height=600
    )

    fig.show()
plot_model(df_train['WIND'], df_val['WIND'], df_test['WIND'], test_pred, 'MLP')

Red de memoria a largo plazo (LSTM)#

from keras.layers import Dense, Input, Dropout
from keras.layers import LSTM
from keras.optimizers import SGD
from keras.models import Model
from keras.models import load_model
from keras.callbacks import ModelCheckpoint

Para proceder con el LSTM en primer lugar se reconfigura la dimesnión de los subconjuntos de datos:

X_train, X_val, X_test = X_train.reshape((X_train.shape[0], X_train.shape[1], 1)), 
                                        X_val.reshape((X_val.shape[0], X_val.shape[1], 1)), 
                                        X_test.reshape((X_test.shape[0], X_test.shape[1], 1))

print('Shape of 3D arrays:', X_train.shape, X_val.shape, X_test.shape)
  Cell In[24], line 2
    X_val.reshape((X_val.shape[0], X_val.shape[1], 1)),
    ^
IndentationError: unexpected indent

Creamos la capa de entrada:

input_layer = Input(shape=(5,1), dtype='float64')

Las xapas ocultas:

lstm_layer1 = LSTM(64, input_shape=(7,1), return_sequences=True)(input_layer)
lstm_layer2 = LSTM(32, input_shape=(7,64), return_sequences=False)(lstm_layer1)

Del mismo modo que el MLP, se añade una capa Dropout antes de la capa de salida, con p = 20% de las características de entrada seleccionadas aleatoriamente.

dropout_layer = Dropout(0.2)(lstm_layer2)

Y por último, la capa de salida con una neurona y función de activación lineal:

output_layer = Dense(1, activation='linear')(dropout_layer)
ts_model = Model(inputs=input_layer, outputs=output_layer)
ts_model.compile(loss='mean_absolute_error', optimizer='adam') #SGD(lr=0.001, decay=1e-5))
ts_model.summary()
Model: "model_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_2 (InputLayer)        [(None, 5, 1)]            0         
                                                                 
 lstm (LSTM)                 (None, 5, 64)             16896     
                                                                 
 lstm_1 (LSTM)               (None, 32)                12416     
                                                                 
 dropout_1 (Dropout)         (None, 32)                0         
                                                                 
 dense_4 (Dense)             (None, 1)                 33        
                                                                 
=================================================================
Total params: 29,345
Trainable params: 29,345
Non-trainable params: 0
_________________________________________________________________
save_weights_at = os.path.join('keras_models', 'WS_LSTM_weights.{epoch:02d}-{val_loss:.4f}.keras')
save_best = ModelCheckpoint(save_weights_at, monitor='val_loss', verbose=0,
                            save_best_only=True, save_weights_only=False, mode='min', save_freq='epoch')
history_WS_LSTM = None

if os.path.exists('history_WS_LSTM.joblib'):
    history_WS_LSTM = load('history_WS_LSTM.joblib')
    print("El archivo 'history_WS_LSTM.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_WS_LSTM = ts_model.fit(x=X_train, y=y_train, batch_size=32, epochs=40,
             verbose=2, callbacks=[save_best], validation_data=(X_val, y_val),
             shuffle=True);
    dump(history_WS_LSTM.history, 'history_WS_LSTM.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_WS_LSTM.joblib'.")
Epoch 1/40
183/183 - 23s - loss: 4.6405 - val_loss: 4.0077 - 23s/epoch - 125ms/step
Epoch 2/40
183/183 - 2s - loss: 3.4726 - val_loss: 3.4897 - 2s/epoch - 13ms/step
Epoch 3/40
183/183 - 4s - loss: 3.2920 - val_loss: 3.3890 - 4s/epoch - 22ms/step
Epoch 4/40
183/183 - 3s - loss: 3.2869 - val_loss: 3.2871 - 3s/epoch - 14ms/step
Epoch 5/40
183/183 - 2s - loss: 3.2419 - val_loss: 3.2934 - 2s/epoch - 13ms/step
Epoch 6/40
183/183 - 3s - loss: 3.2426 - val_loss: 3.2473 - 3s/epoch - 14ms/step
Epoch 7/40
183/183 - 2s - loss: 3.2501 - val_loss: 3.2898 - 2s/epoch - 11ms/step
Epoch 8/40
183/183 - 4s - loss: 3.2240 - val_loss: 3.2302 - 4s/epoch - 21ms/step
Epoch 9/40
183/183 - 2s - loss: 3.2350 - val_loss: 3.2276 - 2s/epoch - 11ms/step
Epoch 10/40
183/183 - 2s - loss: 3.2266 - val_loss: 3.2832 - 2s/epoch - 12ms/step
Epoch 11/40
183/183 - 3s - loss: 3.2257 - val_loss: 3.3607 - 3s/epoch - 15ms/step
Epoch 12/40
183/183 - 2s - loss: 3.2162 - val_loss: 3.2110 - 2s/epoch - 13ms/step
Epoch 13/40
183/183 - 2s - loss: 3.2107 - val_loss: 3.2413 - 2s/epoch - 11ms/step
Epoch 14/40
183/183 - 4s - loss: 3.2175 - val_loss: 3.2394 - 4s/epoch - 22ms/step
Epoch 15/40
183/183 - 2s - loss: 3.2084 - val_loss: 3.2170 - 2s/epoch - 10ms/step
Epoch 16/40
183/183 - 2s - loss: 3.2070 - val_loss: 3.2111 - 2s/epoch - 10ms/step
Epoch 17/40
183/183 - 2s - loss: 3.1924 - val_loss: 3.2215 - 2s/epoch - 13ms/step
Epoch 18/40
183/183 - 3s - loss: 3.2160 - val_loss: 3.2266 - 3s/epoch - 14ms/step
Epoch 19/40
183/183 - 4s - loss: 3.2016 - val_loss: 3.2871 - 4s/epoch - 23ms/step
Epoch 20/40
183/183 - 2s - loss: 3.1953 - val_loss: 3.2666 - 2s/epoch - 10ms/step
Epoch 21/40
183/183 - 2s - loss: 3.2171 - val_loss: 3.2279 - 2s/epoch - 11ms/step
Epoch 22/40
183/183 - 2s - loss: 3.2053 - val_loss: 3.2131 - 2s/epoch - 11ms/step
Epoch 23/40
183/183 - 2s - loss: 3.2169 - val_loss: 3.2316 - 2s/epoch - 12ms/step
Epoch 24/40
183/183 - 2s - loss: 3.2005 - val_loss: 3.2535 - 2s/epoch - 13ms/step
Epoch 25/40
183/183 - 2s - loss: 3.2089 - val_loss: 3.2146 - 2s/epoch - 12ms/step
Epoch 26/40
183/183 - 4s - loss: 3.1867 - val_loss: 3.2512 - 4s/epoch - 23ms/step
Epoch 27/40
183/183 - 2s - loss: 3.1901 - val_loss: 3.2275 - 2s/epoch - 10ms/step
Epoch 28/40
183/183 - 2s - loss: 3.1899 - val_loss: 3.2076 - 2s/epoch - 10ms/step
Epoch 29/40
183/183 - 4s - loss: 3.2074 - val_loss: 3.2640 - 4s/epoch - 21ms/step
Epoch 30/40
183/183 - 3s - loss: 3.1766 - val_loss: 3.2298 - 3s/epoch - 14ms/step
Epoch 31/40
183/183 - 2s - loss: 3.1886 - val_loss: 3.2277 - 2s/epoch - 12ms/step
Epoch 32/40
183/183 - 2s - loss: 3.1986 - val_loss: 3.2641 - 2s/epoch - 12ms/step
Epoch 33/40
183/183 - 2s - loss: 3.1825 - val_loss: 3.2198 - 2s/epoch - 10ms/step
Epoch 34/40
183/183 - 2s - loss: 3.1806 - val_loss: 3.2197 - 2s/epoch - 11ms/step
Epoch 35/40
183/183 - 4s - loss: 3.1622 - val_loss: 3.2725 - 4s/epoch - 22ms/step
Epoch 36/40
183/183 - 2s - loss: 3.1889 - val_loss: 3.2645 - 2s/epoch - 12ms/step
Epoch 37/40
183/183 - 2s - loss: 3.1799 - val_loss: 3.2091 - 2s/epoch - 13ms/step
Epoch 38/40
183/183 - 2s - loss: 3.1783 - val_loss: 3.3309 - 2s/epoch - 12ms/step
Epoch 39/40
183/183 - 2s - loss: 3.1602 - val_loss: 3.2941 - 2s/epoch - 11ms/step
Epoch 40/40
183/183 - 2s - loss: 3.1890 - val_loss: 3.2086 - 2s/epoch - 11ms/step
El entrenamiento se ha completado y el historial ha sido guardado en 'history_WS_LSTM.joblib'.
model_dir = 'keras_models'
files = os.listdir(model_dir)
pattern = r"WS_LSTM_weights\.(\d+)-([\d\.]+)\.keras"
    
best_val_loss = float('inf')
best_model_file = None
best_model = None

for file in files:
    match = re.match(pattern, file)
    if match:
        epoch = int(match.group(1))
        val_loss = float(match.group(2))
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_model_file = file

if best_model_file:
    best_model_path = os.path.join(model_dir, best_model_file)
    print(f"Cargando el mejor modelo: {best_model_file} con val_loss: {best_val_loss}")
    best_model = load_model(best_model_path)
else:
    print("No se encontraron archivos de modelos que coincidan con el patrón.")
Cargando el mejor modelo: WS_LSTM_weights.28-3.2076.keras con val_loss: 3.2076
index = 'LSTM val model'

val_result = best_model.predict(X_val)
val_pred = val_result[:, 0]

x_val = df_val['WIND'].loc[5:]

mae = mean_absolute_error(x_val, val_pred)
sse = np.sum((x_val - val_pred) ** 2)
mape = np.mean(np.abs((x_val-val_pred) / x_val)) * 100
msd = mean_squared_error(x_val, val_pred)
r2 = r2_score(x_val, val_pred) 

ljung_box = acorr_ljungbox(x_val - val_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(x_val- val_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
12/12 [==============================] - 2s 8ms/step
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
LSTM model 3.207639 6117.322011 32.539459 16.992561 0.274262 0.345153 0.001138

Para el modelo LSTM, un MAE de 3.21 sugiere que las predicciones presentan una desviación promedio de 3.21 nudos respecto a los valores reales, lo que indica una precisión relativamente buena en comparación con otros modelos evaluados. La SSE de 6117.32 evidencia un error acumulado moderado, mientras que un MAPE de 32.54% muestra que las predicciones tienen una precisión porcentual aceptable. La MSD de 16.99, ligeramente superior a la del MLP, respalda la existencia de algunos errores residuales en el modelo. El coeficiente R² de 0.27 sugiere que el modelo es capaz de explicar una proporción limitada de la variabilidad de la serie temporal, aunque el ajuste sigue siendo insuficiente. Los p-valores de las pruebas de Ljung-Box (0.345) y de normalidad (0.001) indican que los residuos no presentan una autocorrelación significativa, pero siguen sin ajustarse a una distribución normal. En resumen, este modelo LSTM ofrece una predicción adecuada y capta patrones de la serie temporal de manera aceptable, aunque aún presenta ciertas limitaciones en cuanto a su precisión total y ajuste estadístico.

index = 'LSMT model'

test_result =  best_model.predict(X_test)
test_pred = test_result[:, 0]  

x_test = df_test['WIND'].loc[5:]

mae = mean_absolute_error(x_test, test_pred)
sse = np.sum((x_test - test_pred) ** 2)
mape = np.mean(np.abs((x_test - test_pred) / x_test)) * 100
msd = mean_squared_error(x_test, test_pred)
r2 = r2_score(x_test,test_pred) 

ljung_box = acorr_ljungbox(x_test - test_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(x_test - test_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
12/12 [==============================] - 0s 9ms/step
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
LSMT model 3.273242 6286.043533 57.099386 17.461232 0.353676 0.757025 0.000095

Para el modelo de prueba de LSTM, un MAE de 3.27 sugiere que las predicciones presentan una desviación promedio de 3.27 nudos respecto a los valores reales, lo cual es razonablemente preciso. La SSE de 6286.04 refleja un error acumulado moderado, y un MAPE de 57.10% indica que, en términos porcentuales, la precisión es mejorable. La MSD de 17.46 confirma una presencia significativa de errores, mientras que un R² de 0.35 indica que el modelo es capaz de explicar una fracción limitada de la variabilidad de la serie temporal. El p-valor de la prueba de Ljung-Box (0.757) sugiere que los residuos no tienen autocorrelación significativa, lo que es positivo para la aleatoriedad de los errores, aunque el p-valor de la prueba de normalidad (0.000095) confirma que estos residuos no se distribuyen normalmente. En resumen, aunque el modelo LSTM captura algunos patrones de la serie, su ajuste global y precisión porcentual podrían mejorarse.

plot_model(df_train['WIND'], df_val['WIND'], df_test['WIND'], test_pred, 'LSTM')